﻿namespace NerdWare.Crm2011VsTools.CrmScriptInjector
{
    using System;
    using System.IO;
    using System.Reflection;
    using System.Text.RegularExpressions;

    using Fiddler;

    using NerdWare.Crm2011VsTools.CrmScriptInjector.Configuration;
    using NerdWare.Crm2011VsTools.CrmScriptInjector.EventArgs;

    public sealed class ScriptProxy: IDisposable
    {
        private string _hostname;
        private int _port;
        private string _organization;


        private bool _fiddlerCertInstalled;
        private bool _fiddlerCertTrusted;

        private readonly FileMonitor _fileMonitor = new FileMonitor();

        public event EventHandler<UrlChangedEventArgs> UrlChanged;
        public event EventHandler<MessageRaisedEventArgs> MessageRaised;

        public ScriptProxy()
        {
            this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Debug, "ScriptProxy initialized."));
            FiddlerApplication.BeforeRequest += this.ProxyBeforeRequest;
            FiddlerApplication.OnClearCache += OnOnClearCache;
        }

        private void OnOnClearCache(object sender, CacheClearEventArgs cacheClearEventArgs)
        {
            this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Info, "Cache gets cleared."));
        }

        private void OnUrlChanged(UrlChangedEventArgs e)
        {
            if (UrlChanged != null)
            {
                UrlChanged(this, e);
            }
        }

        private void OnMessagedRaised(MessageRaisedEventArgs e)
        {
            if (MessageRaised != null)
            {
                MessageRaised(this, e);
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is running.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is running; otherwise, <c>false</c>.
        /// </value>
        public bool IsRunning
        {
            get { return FiddlerApplication.IsStarted(); }
        }

        /// <summary>
        /// Starts the proxy.
        /// </summary>
        /// <param name="config">The config.</param>
        public void StartProxy(Config config)
        {
            if (!FiddlerApplication.IsStarted())
            {
                WinINETCache.ClearCacheItems(true, true);

                _hostname = config.CrmServerUri.Host.ToLower(); //hostname.ToLower();
                _port = config.CrmServerUri.Port; // port;
                _organization = config.OrganizationName.ToLower();

                UriRegEx = this.GenerateRegEx();

                _fileMonitor.GetFiles(config.ScriptPath);

                FiddlerCoreStartupFlags captureFlags = FiddlerCoreStartupFlags.RegisterAsSystemProxy |
                                                       FiddlerCoreStartupFlags.MonitorAllConnections;
                if (_port == 443)
                {
                    InstallCerts();
                    captureFlags = captureFlags | FiddlerCoreStartupFlags.DecryptSSL;
                }

                FiddlerApplication.Startup(config.FiddlerPort, captureFlags);

                this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Info, "Fiddler Proxy started."));
            }

        }

        /// <summary>
        /// Stops the proxy.
        /// </summary>
        public void StopProxy()
        {
            if (FiddlerApplication.IsStarted())
            {
                FiddlerApplication.Shutdown();
                this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Info, "Fiddler Proxy stopped."));
            }
        }

        private void ProxyBeforeRequest(Session osession)
        {
            //osession.oRequest.headers["User-Agent"] = "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0)";

            if (osession.hostname.ToLower() == _hostname && osession.port == _port)
            {
                if (osession.PathAndQuery.ToLower().Contains("webresources"))
                {
                    Match match = UriRegEx.Match(osession.PathAndQuery.ToLower());

                    if (match.Success)
                    {
                        this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Debug, "PathAndQuery '{0}' matches.", osession.PathAndQuery));

                        string key = match.Groups[Groups.Filename].Value;

                        if (!key.EndsWith(".js"))
                            key = string.Concat(key, ".js");

                        //this.OnUrlChanged(new UrlChangedEventArgs(osession, key));

                        switch (_fileMonitor.ContainCounter(key))
                        {
                            case 0:
                                this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Verbose, "Match {0} has no possible matching Source-File.", key));
                                break;
                            case 1:
                                this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Info, "Match {0} gets replaced by the local copy.", key));
                                //osession.bBufferResponse = true;
                                osession.utilCreateResponseAndBypassServer();
                                osession.oResponse.headers.HTTPResponseStatus = "200 Ok";
                                osession.oResponse["Content-Type"] = "text/javascript; charset=UTF-8";
                                osession.oResponse["Cache-Control"] = "private, max-age=0";
                                osession.utilSetResponseBody(_fileMonitor.GetFileContent(key));

                                this.OnUrlChanged(new UrlChangedEventArgs(osession, key));
                                break;
                            default:
                                this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Warn, "Match {0} has {1} possible matching Source-Files.", key, _fileMonitor.ContainCounter(key)));
                                break;
                        }
                    }
                    else
                    {
                        this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Debug, "PathAndQuery '{0}' was no Match in the Expression.", osession.PathAndQuery));
                    }
                }
                else
                {
                    this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Debug, "PathAndQuery '{0}' doesn't contain the word 'webresources'", osession.PathAndQuery));
                }
            }
            else
            {
                this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Debug, "Watching for Hostname '{0}' and Port [{1}] but found Hostname '{2}' and Port [{3}]",
                    _hostname, _port, osession.hostname, osession.port));
            }
        }

        private Regex UriRegEx { get; set; }

        private Regex GenerateRegEx()
        {
            //switch (_crmType)
            //{
            //    case CrmType.Crm2011OnPremise:
            //    case CrmType.Crm2011OnlineNorthAmericaLiveId:
            //    case CrmType.Crm2011OnlineNorthAmericaOffice365:
            //    case CrmType.Crm2011OnlineEMEALiveId:
            //    case CrmType.Crm2011OnlineEMEAOffice365:
            //    case CrmType.Crm2011OnlineAPACLiveId:
            //    case CrmType.Crm2011OnlineAPACOffice365:

            return new Regex(@"(\/(?<" + Groups.Organization + @">" + _organization + @")){0,1}\/%7b\d+%7d\/webresources\/(?<" + Groups.Path + @">.+\/)*(?<" + Groups.Filename + @">.+(\.js){0,1})");
            //default:
            //    throw new NotImplementedException("This Crm Type is not yet implemented.");
            // }
        }

        private void InstallCerts()
        {
            try
            {
                HighJackMakeCertPath(CertMaker.oCertProvider);

                if (!CertMaker.rootCertExists())
                {
                    this.OnMessagedRaised(
                        new MessageRaisedEventArgs(MessageType.Verbose, "Certificates do not exist. Trying to install..."));

                    if (!CertMaker.createRootCert())
                    {
                        this.OnMessagedRaised(
                        new MessageRaisedEventArgs(MessageType.Error, "Unable to create certificate for FiddlerCore."));
                        throw new ApplicationException("Unable to create certificate for FiddlerCore.");
                    }
                    else
                    {
                        this.OnMessagedRaised(
                        new MessageRaisedEventArgs(MessageType.Verbose, "Certificates installed successfully."));
                        _fiddlerCertInstalled = true;
                    }
                }
                else
                {
                    _fiddlerCertInstalled = true;
                }

                if (!CertMaker.rootCertIsTrusted())
                {
                    this.OnMessagedRaised(
                        new MessageRaisedEventArgs(MessageType.Verbose, "Certificate not trusted. Trying to trust..."));

                    if (!CertMaker.trustRootCert())
                    {
                        this.OnMessagedRaised(
                            new MessageRaisedEventArgs(MessageType.Error, "Unable to install FiddlerCore's certificate."));
                        throw new ApplicationException("Unable to install FiddlerCore's certificate.");
                    }
                    else
                    {
                        this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Verbose, "Certificate is now trusted."));
                        _fiddlerCertTrusted = true;
                    }

                    _fiddlerCertTrusted = true;
                }
                else
                {
                    _fiddlerCertTrusted = true;
                }
            }
            catch (Exception ex)
            {
                this.OnMessagedRaised(
                        new MessageRaisedEventArgs(MessageType.Error, ex.Message));
            }
        }

        private void RemoveCerts()
        {
            if (_fiddlerCertTrusted || _fiddlerCertInstalled)
            {
                CertMaker.removeFiddlerGeneratedCerts(true);

                _fiddlerCertInstalled = false;
                _fiddlerCertTrusted = false;

                this.OnMessagedRaised(new MessageRaisedEventArgs(MessageType.Verbose, "Removed Certificate."));
            }
        }

        private void HighJackMakeCertPath(ICertificateProvider certificateProvider)
        {
            Type myType = certificateProvider.GetType();

            const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

            FieldInfo certLocation = myType.GetField("_sMakeCertLocation", flags);
            if (certLocation != null)
            {
                string makeCertLocation = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName, "MakeCert.Exe");

                if (certLocation.GetValue(certificateProvider).ToString() != makeCertLocation)
                {
                    //NTrace.Default.Info("Overwriting MakeCert-Path from '{0}' with '{1}'", certLocation.GetValue(certificateProvider), makeCertLocation);
                    certLocation.SetValue(certificateProvider, makeCertLocation);
                }
            }
        }

        public void Dispose()
        {
            if (FiddlerApplication.IsStarted())
            {
                FiddlerApplication.Shutdown();
            }

            if (this._port == 443)
            {
                //this.RemoveCerts();
            }

            FiddlerApplication.BeforeRequest -= this.ProxyBeforeRequest;
        }
    }
}
